iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0

閉包 (Closures)

之前我們已經知道了執行脈絡與執行堆疊,
這兩個觀念都對理解閉包起了很大的幫助,

先來看一段利用到閉包的程式碼:

function greet(whattosay) {
  return function(name) {
    console.log(whattosay + " " + name);
  }
}

var sayHi = greet("Hi");
sayHi("Jimmy");

在開發者工具的 console 中查看輸出結果:

會看到成功輸出結果,
但是如果仔細想一下這段程式碼在實行 var sayHi = greet("Hi");
後,會產生 greet function 中執行脈絡來執行 function 中的程式,

當 function 中的程式碼結束時會回傳一個匿名 function ,
並且 greet function 的執行脈絡會離開執行堆疊,

那這樣為什麼在執行 sayHi() 時,可以參考到 greet function 中的 whattosay 參數(也可以算是變數)呢?

一般情況下當執行脈絡從執行堆疊離開後 JavaScript 引擎會將執行完畢的執行脈絡占用的記憶體位址進行垃圾回收(garbage collection),

但 greet function 的執行脈絡中的變數環境還在記憶體中:

接著執行 sayHi() 時會將執行脈絡放進執行堆疊來執行程式碼:

因為在 sayHi 中找不到 whattosay 變數,
所以透過執行脈絡的外部環境往 Scope Chain 裡面尋找,
結果會在記憶體中找到變數 whattosay,

這有點像是 sayHi function 的執行脈絡關住、包住了外部變數 whattosay
:

呼叫 sayHi function 執行時之所以取用的到變數 whattosay 的原因,
是因為執行完 function 後,
被執行完的 function 當中的變數還存留在記憶體中的特性,
這種特性在 JavaScript 中就稱作閉包.


再來看一些其他例子:

function buildFunctions() {
  var arr = [];

  for (var i = 0; i < 3; i++) {
    arr.push(function () {
      console.log(i);
    });
  }

  return arr;
}

var fs = buildFunctions();

fs[0]();
fs[1]();
fs[2]();

現在來看一下這段程式碼發生了什麼事:

JavaScript 引擎產生全域執行脈絡時會先經歷創造階段,
在創造階段也會產生全域物件、this 與外部環境,
會先逐行解析程式碼將 function 與變數分配記憶體空間(Hoisting),
一開始發現有宣告一個 buildFunctions function,
因此會將這整個 buildFunctions function 放進全域執行脈絡記憶體中,

然後發現有透過 var 宣告 fs 時也會在全域執行脈絡的變數環境新增 fs 並設置預設值 undefined:

在執行階段時會把全域執行脈絡放入執行堆疊中來執行全域執行脈絡中的程式碼,
當執行到 var fs = buildFunctions(); 時,
會創造 buildFunctions function 的執行脈絡,
buildFunctions function 的執行脈絡一樣會先經過創造階段,
產生 this 與外部環境,並且會替變數設置記憶體空間與預設值 undefined:

接著進入執行階段把 buildFunctions function 的執行脈絡放進執行堆疊中來執行 buildFunctions function 的程式碼:

這邊說明執行 buildFunctions function 時程式碼的執行過程:
會先跑 3次 for 迴圈,
每次都會把匿名 function 放進陣列 arr 中,
匿名 function 的程式碼執行後會印出 i 的值,
但匿名 function 還沒有被執行只是先被創造,
當 for 迴圈執行到 i 等於 3 時因為不符合條件就會停止,
因此在執行環境的 i 的變數環境中 i 的值為 3,
for 迴圈跑完 3 次後 arr 陣列中會有三個匿名 function,
最後會回傳 arr 陣列,
執行結束後,
buildFunctions function 的執行脈絡會從執行堆疊中離開,

這時回到全域執行脈絡中,
fs 的記憶體空間中會是 arr 陣列(值是三個匿名 function):

繼續執行 fs[0]() 時,
會執行 arr 陣列中的第一個匿名 function 並創造執行脈絡,
執行脈絡在創造階段時,沒有解析到需要放到記憶體中(hoisting)的變數或 function ,
之後將執行脈絡放進執行堆疊中進入執行階段執行程式碼:

因為在自己的執行脈絡裡的變數環境找不到變數 i,
因此會透過外部環境往 Scope Chain 中找參考到的執行脈絡裡的變數 i,
雖然 buildFunctions function 的執行脈絡已經離開執行堆疊,
且離開時變數 i 在記憶體位址中的值停留在 3,
因此會找到這個變數 i 並形成閉包把外部環境參考到的變數關住、包住:

因此輸出變數 i 的值是 3,
接下來執行陣列中的剩下兩個匿名 function 也是一樣的過程,
因此在開發者工具中的 console 中才會看到輸出 3 個 3:

如果想要讓輸出結果分別為 0 1 2 可以透過兩種方式

方法1.
在 for 迴圈中宣告一個新變數並把 i 賦直給這個變數:

function buildFunctions() {
  var arr = [];

  for (var i = 0; i < 3; i++) {
    let j = i;
    arr.push(function () {
      return (function (j) {
        console.log(j);
      }(i));
    });
  }

  return arr;
}

var fs = buildFunctions();

fs[0]();
fs[1]();
fs[2]();

方法2.
將 for 迴圈中放進 arr 陣列的匿名 function 包在 IIFE 中當作回傳值, IIFE 呼叫時傳入變數 i 當作參數,
IIFE 執行後會產生自己的執行脈絡與自己的變數環境,
變數環境中的記憶體位址的值會參考到每次 for 迴圈執行時 i 的值,
因為 IIFE 回傳的匿名 function 的執行脈絡會透過外部環境到 Scope Chain 中找 IIFE 的執行脈絡中變數 j 在記憶體中的位址並把它關住、包住,
在記憶體位址中的值是每次 for 迴圈執行時傳入 IIFE 的變數 i 的值:

function buildFunctions() {
  var arr = [];

  for (var i = 0; i < 3; i++) {
    arr.push(
      (function (j) {
        return function () {
          console.log(j);
        }
      }(i))
    );
  }

  return arr;
}

var fs = buildFunctions();

fs[0]();
fs[1]();
fs[2]();

在開發者工具的 console 中來查看預期結果:


上一篇
立即執行函數 IIFE
下一篇
原型鏈(Prototype Chain)
系列文
那些必須了解的 JavaScript 特性與寫程式前的思考17
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言